代码分割的意义

对于大型的web应用来说,将所有的代码都放在一个文件中显然是不够有效的,特别是当项目中某些代码在首页中并不需要使用而是只有在某些特殊的时候才会被使用到。webpack有一个功能是将项目的代码分割成chunks(代码块),当代码运行到需要它们的时候再进行加载。

适用场景:

  • 抽离相同代码到公共代码块;
  • 代码懒加载,使得初始下载的代码体积更小。

基础库的分离

思路:将项目中使用到的react和react-dom等基础包通过cdn引入,不打入bundle中。

使用html-webpack-externals-plugin

// 装包
yarn add html-webpack-externals-plugin -D
1
2
// 引包
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
1
2
// 配置
new HtmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'react',
        // entry可以是本地文件也可以是cnd文件
        // 推荐使用cdn资源
        entry: 'https://cdn.bootcss.com/react/16.10.2/umd/react.production.min.js',
        global: 'React',
      },
      {
        module: 'react-dom',
        entry: 'https://cdn.bootcss.com/react-dom/16.10.2/umd/react-dom.production.min.js',
        global: 'ReactDOM',
      },
    ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 还需要在模板文件中引入如下cdn资源
<script src="https://cdn.bootcss.com/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
1
2
3

splitChunks分离react基础库

optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                test: /(react|react-dom)/,
                name: 'vendors',
                chunks: 'all'
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

打包结果如下:

                Asset       Size  Chunks                         Chunk Names
img/logo_bd62f047.png   8.54 KiB          [emitted]
           index.html  359 bytes          [emitted]
    index_ae13cefb.js  987 bytes       0  [emitted] [immutable]  index
          search.html   1.52 KiB          [emitted]
   search_59239433.js    8.9 KiB       1  [emitted] [immutable]  search
  search_eef75cac.css  127 bytes       1  [emitted] [immutable]  search
  vendors_45bf2ea6.js    121 KiB       2  [emitted] [immutable]  vendors
1
2
3
4
5
6
7
8

还需要在htmlWebpackPlugins中配置对应的chunks:

chunks: ['vendors', pathName]
1

设置最小引用次数

optimization: {
    splitChunks: {
        minSize: 0,
        cacheGroups: {
            commons: {
                name: 'commons',
                chunks: 'all',
                minChunks: 2 // 至少被两个页面入口引用
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

分别在两个入口文件中引入对应的模块,重新打包,结果如下:commons_071961a9.js文件被单独分离出来。

                Asset       Size  Chunks                         Chunk Names
  commons_071961a9.js   91 bytes       0  [emitted] [immutable]  commons
img/logo_bd62f047.png   8.54 KiB          [emitted]
           index.html  293 bytes          [emitted]
    index_2ded2e28.js   1.55 KiB       1  [emitted] [immutable]  index
          search.html   1.46 KiB          [emitted]
  search_eef75cac.css  127 bytes       2  [emitted] [immutable]  search
   search_f313c9b7.js    130 KiB       2  [emitted] [immutable]  search
1
2
3
4
5
6
7
8
// 第一种方式:
import _ from 'lodash'; // 假设lodash库代码是 1MB

// 假设业务逻辑代码也是 1MB
// 那么打包后的代码体积就是2MB,用户想要看到页面的内容,需要加载2MB的js文件

// 这样的问题在于:打包文件会很大,加载时间会很长
console.log(_.join(['a', 'c', 'e'], '!!!'));

// 但是像lodash这样的第三方模块,我们一般不会进行变更
// 我们只会更改我们的业务代码
// 但是现在的情况是,我们只要改动了业务代码,重新访问页面时,又要加载2MB的内容才能看到更新后的内容,这样是有问题的。

// 第二种方式:
// 将main.js拆分成两个入口:lodash.js(1MB)和main.js(1MB)
// 这样拆分之后,当页面业务逻辑发生变化时,只要重新加载main.js(1MB)即可。因为lodash.js在浏览器中是有缓存的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
optimization: {
    splitChunks: {
        chunks: 'all' // all表示同步和异步代码都进行代码分割
    }
}
1
2
3
4
5

vendors~main.698faa5f0aa1734e5e70.js中是lodash的代码,而main.698faa5f0aa1734e5e70.js中是我们的业务代码。配置代码分割后,webpack将自动为我们进行代码的分割。

异步代码 Code Splitting

// 异步代码的Code Splitting
function getComponent() {
    return import('lodash').then(({default: _}) => {
        const element = document.createElement('div');
        element.innerText = _.join(['hello', 'webpack'], '---');
        return element;
    });
}

getComponent().then(element => {
    document.body.appendChild(element);
});
1
2
3
4
5
6
7
8
9
10
11
12

异步以jsonp的形式加载lodash。

小结

Code Splitting并不是webpack独有的概念,通过合理的Code Splitting,可以使得我们的项目运行的性能更高。需要记住:代码分割和webpack无关。

webpack中实现代码分割,有两种方式:

  • 同步代码:只需要在webpack中配置optimization即可
  • 异步代码(import):无需做任何配置,会自动进行代码分割,放置到新的文件中。

使用SplitChunksPlugin进行公共脚本分离

可以通过魔法注释来给异步加载的模块指定相应的名字。

进行上图的配置后,生成的文件就叫lodash.js了,而不是vendors~lodash.js。

chunks: 'async'

  • async(默认):只对异步引入的代码进行分割
  • initial:只对同步引入的代码进行分割
  • all(推荐):对所有(包括同步和异步)引入代码都进行分割

如果配置chunks: 'async',则下面同步引入的lodash将不会进行代码分割。

 optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      automaticNameMaxLength: 30,
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

常用参数介绍

  • minSize(默认是30000,单位是字节):形成一个新代码块最小的体积;
  • minChunks(默认是1,即当前代码块只被一个页面引用):在分割之前,代码块最小被引用的次数;
  • maxInitialRequests(默认是3):一个入口最大的并行加载文件数;
  • maxAsyncRequests(默认是5):按需加载的时候,浏览器最大的并行请求数;
  • chunks(默认是async) :用于配置控制webpack选择哪些代码块用于分割(译注:其他类型代码块按默认方式打包),有3个可选的值:initial(初始块)、async(按需加载的异步块)和all(所有块)async表示作用于异步模块,all表示作用于所有模块,initial表示作用于同步模块;
  • test:用于规定缓存组匹配的文件位置,test: /node_modules/,即为匹配相应文件夹下的模块。原封不动传递出去的话,它默认会选择所有的模块。可以传递的值类型:RegExp、String和Function
  • name(打包的chunks的名字) :字符串或者函数(函数可以根据条件自定义名字);用以控制分离后代码块的命名,当存在匹配的缓存组时,命名使用缓存组中的name值,若不存在则为[来源]~[入口的key值].js的格式。
  • priority:缓存组打包的先后优先级。这个是最重要的,即便是所有配置项都写好了,优先级不够,或者优先级设置不正确,也得不到相应的结果。当需要优先匹配缓存组的规则时,priority需要设置为正数,当需要优先匹配默认设置时,缓存组需设置为负数,0为两者的分界点。
  • reuseExistingChunk:设置该选项允许复用已经存在的代码块,而不是新建一个新的,需要在精确匹配到对应模块时候才会生效;
  • automaticNameDelimiter:修改上文中的~,若改为-,则分离后的js默认命名规则为[来源]-[入口的key值].js
  • cacheGroups:即缓存组,其实就是存放分离代码块的规则的对象,叫做cacheGroup的原因是webpack会将规则放置在cache流中,为对应的块文件匹配对应的流,从而生成分离后的块。cacheGrouppriority为分离规则的优先级,优先级越高,则优先匹配。

vendors~main.js表示:该分割的代码数组vendors这个缓存组,并且属于main这个入口文件。

filename配置打包后的名称。

同步代码分割

同步代码先走chunks配置,然后看符合哪个缓存组,然后再走对应的缓存组的配置。

// test.js,并在index.js中引入
export const name = 'lisi';
1
2
optimization: {
        splitChunks: {
            // chunks: 'all', // all表示同步和异步代码都进行代码分割
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            // },
            chunks: 'all', // 默认是只对异步代码进行代码分割
            minSize: 0, // 因为test.js文件很小,所以这里设置为0
            maxSize: 0, // 一般不用
            minChunks: 1, // 模块的最小引用次数
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            automaticNameMaxLength: 30,
            name: true,
            // cacheGroups: {
            //     vendors: false,
            //     default: false
            // }
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    filename: 'vendors.js'
                },
                // default: false
                default: {
                    // minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

重命名:

chunks: 'async', // 默认是只对异步代码进行代码分割
minSize: 30000, // 引入的模块大于30kb的时候才代码分割
maxSize: 0, // 一般不用
minChunks: 1, // 模块的最小引用次数
maxAsyncRequests: 5, // 同时加载的文件数为5个
maxInitialRequests: 3,
automaticNameDelimiter: '~', // 文件名之间的连接符
automaticNameMaxLength: 30, // 文件名最大的长度
name: true, // 可以对生成的文件进行重命名,如在cacheGroups配置filename
1
2
3
4
5
6
7
8
9

cacheGroups理解

cacheGroups: {
    vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10, // 值越大,优先级越高
        filename: 'vendors.js'
    },
    // default: false
    default: {
        // minChunks: 2,
        priority: -20,
        reuseExistingChunk: true, // 如果引用的模块之前被打包过了则直接使用,而不会进行重复打包
        filename: 'common.js'
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

假如在代码中同时引入了lodash和jquery,这两个都符合vendors缓存组的条件,webpack会在代码都分析完后,将所有符合各个缓存组条件的模块一起打包。而不是匹配到一个就进行打包一次。

使用webpack4在打包多页面应用过程中,需要提取公共代码。相比于webpack3而言,4.0版本用optimization.splitChunks配置替换了3.0版本的CommonsChunkPlugin插件。在使用和配置上,更加方便和清晰。

在使用splitChunksPlugins之前,首先要知道splitChunksPluginswebpack主模块中的一个细分模块,无需npm引入。功能上,splitChunksPlugins只能用于如何抽离公用的代码,也就是抽离公用代码的规则,要记住,除了这个功能之外,splitChunksPlugins再无其他功能。

配置缓存组(Configurate cache group)

默认配置如下:

splitChunks: {
    chunks: 'async',
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    name: true,
    cacheGroups: {
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
        },
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

默认来说,cacheGroups(缓存组)会继承splitChunks的配置,但是test、priorty和reuseExistingChunk只能用于配置缓存组。

cacheGroups是一个对象,按上述介绍的键值对方式来配置即可,值代表对应的选项。

除此之外,所有上面列出的选项都是可以用在缓存组里的:chunks, minSize,minChunks,maxAsyncRequests,maxInitialRequests,name

可以通过optimization.splitChunks.cacheGroups.default: false,来禁用default缓存组。

default缓存组的优先级(priotity)是负数,因此所有自定义缓存组都可以有比它更高优先级(译注:更高优先级的缓存组可以优先打包所选择的模块),默认自定义缓存组优先级为0。

打包结果:

dead6af70ba7940654bd4086445751a4.png

optimization.runtimeChunk

runtimeChunk: 'single'
// 等价于
runtimeChunk: {
   name: 'runtime'
}
1
2
3
4
5

将webpack的运行文件单独打包到一个代码块中。

通过optimization.runtimeChunk: true选项,webpack会添加一个只包含运行时(runtime)额外代码块到每一个入口。(译注:这个需要看场景使用,会导致每个入口都加载多一份运行时代码)

参考文档

  1. 官方demo
  2. 一步一步的了解webpack4的splitChunk插件